package com.fxu.framework.biz.holder;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.img.Img;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.CannedAccessControlList;
import com.aliyun.oss.model.InitiateMultipartUploadRequest;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import com.fxu.framework.core.base.Result;
import com.fxu.framework.core.enums.BaseEnum;
import com.fxu.framework.core.holder.SpringHolder;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.net.URL;
import java.util.*;

/**
 * 文件存储读取基础类
 * @author fangxu
 * @version 1.2.5
 */
@Slf4j
@Component("fxuUploadHolder")
public final class UploadHolder {
	private static final String[] SCALE_TYPES = { "png","jpg","bmp","gif","jpeg","webp" };
	private static final List<Long> scaleSizes = new ArrayList<>();
	private static final Map<Long, Float> scaleMap = new HashMap<>();
	private static final String DOT = ".";
	private static final String XOR = "&";
	private static final String ANS = "?";
	private static final String MOD = "/";

	private static UploadHolder holder = null;
	public synchronized static UploadHolder getHolder() {
		if (holder == null) {
			holder = SpringHolder.getBean("fxuUploadHolder");
			String temp = holder.getUploadFileScale();
			List<String> list = StrUtil.split(temp, ";");
			for (String s : list) {
				if (StrUtil.isNotEmpty(s) || s.contains(",")) {
					List<String> child = StrUtil.split(s, ",");
					if (child != null && child.size() == 2) {
						int multi = 1;
						if (child.get(0).toLowerCase().endsWith("k")) {
							multi = 1024;
						} else if (child.get(0).toLowerCase().endsWith("m")) {
							multi = 1024 * 1024;
						}
						String tmp1 = child.get(0);
						if (multi > 1) {
							tmp1 = tmp1.substring(0, tmp1.length() - 1);
						}
						Long size = Long.parseLong(tmp1) * multi;
						scaleSizes.add(size);
						scaleMap.put(size, Float.parseFloat(child.get(1)));
					}
				}
			}
			CollUtil.sort(scaleSizes, Comparator.reverseOrder());
			log.debug("fxuUploadHolder.scaleSizes={}", scaleSizes);
			log.debug("fxuUploadHolder.scaleMap={}", scaleMap);
		}
		return holder;
	}

	@Value("${upload.file.type:png,jpg,bmp,gif,jpeg,webp,svg,mp3,mp4,flv,wav,wma,acc,apk,pdf,xls,doc,xlsx,docx}")
	private String uploadFileType;

	// 大于5M 25%压缩比例, 大于3M 50%压缩比例, 大于1M 75%压缩比例
	@Value("${upload.scale:1M,0.75;3M,0.5;5M,0.25;}")
	private String uploadFileScale;

	@Value("${upload.base:}")
	private String uploadBase;
	@Value("${upload.path:}")
    private String uploadPath;
	@Value("${upload.url:}")
	private String uploadUrl;
	
	@Value("${aliyun.access-key:}")
	private String aliyunAccessKey;
	@Value("${aliyun.secret-key:}")
	private String aliyunSecretKey;
	@Value("${aliyun.oss.endpoint:}")
	private String aliyunEndpoint;
	@Value("${aliyun.oss.bucket:}")
	private String aliyunBucket;
	@Value("${aliyun.oss.dns-url:}")
	private String aliyunDnsUrl;

	@Data
	@Builder
	@NoArgsConstructor
	@AllArgsConstructor
	public static class UploadBack {
		private Long id;
		private String msg;
		private String url;
		private String name;
		private String path;
		private File file;
		private long size;

		public UploadBack(String url, String name, long size) {
			this.url = url;
			this.name = name;
			this.size = size;
		}

		public UploadBack(String url, String name, long size, File file) {
			this.url = url;
			this.name = name;
			this.size = size;
			this.file = file;
		}
	}

	public String getUploadFileScale() {
		return uploadFileScale;
	}

	public static float getScale(long fileSize) {
		for (Long scaleSize : scaleSizes) {
			if (fileSize >= scaleSize) {
				return scaleMap.get(scaleSize);
			}
		}
		return 1;
	}

	// url => url, url?xxx => url
	public static String getUrl(String url) {
		return StrUtil.isNotEmpty(url) && url.contains(ANS) ? url.substring(0, url.indexOf(ANS)) : url;
	}

	// zzz/xxx.png => xxx, url?xxx => xxx, url?xxx&yyy => xxx
	public static String getUrlName(String url) {
		if (StrUtil.isEmpty(url) || !url.contains(ANS)) {
			return StrUtil.isEmpty(url) ? url : url.substring(url.lastIndexOf(MOD) + 1);
		}
		if (url.contains(XOR)) {
			return url.substring(url.indexOf(ANS) + 1, url.indexOf(XOR));
		}
		return url.substring(url.indexOf(ANS) + 1);
	}

	// zzz/xxx.png => xxx
	public static String getUrlKey(String url) {
		if (StrUtil.isEmpty(url) || !url.contains(MOD)) return url;
		String name = getUrl(url).substring(url.lastIndexOf(MOD) + 1);
		return StrUtil.isNotEmpty(name) && name.contains(DOT) ? name.substring(0, name.indexOf(DOT)) : name;
	}

	// url?xxx&yyy => yyy
	public static Long getUrlSize(String url) {
		return StrUtil.isNotEmpty(url) && url.contains(XOR) ? Long.parseLong(url.substring(url.indexOf(XOR) + 1)) : 0L;
	}

	// 阿里云OSS
	private OSS ossClient;
	private OSS getInstance() {
		if (ossClient == null) {
			synchronized (UploadHolder.class) {
				ossClient = new OSSClientBuilder().build(aliyunEndpoint, aliyunAccessKey, aliyunSecretKey);
			}
		}
		return ossClient;
	}

	/**
	 * <p>上传文件</p>
	 * @param directory 目录
	 * @param file 上传的文件
	 * @return 上传返回对象
	 */
	public Result<UploadBack> uploadLocal(String directory, MultipartFile file) {
		return uploadLocal(uploadBase, directory, file, true, true);
	}

	/**
	 * <p>上传文件</p>
	 * @param root 根目录
	 * @param directory 目录
	 * @param file 上传的文件
	 * @param rename 是否重命名
	 * @param date 是否增加日期
	 * @return 上传返回对象
	 */
	public Result<UploadBack> uploadLocal(String root, String directory, MultipartFile file, boolean rename, boolean date) {
		if (file == null || file.isEmpty()) {
			return Result.error("参数错误!file=null");
		}
		if (!containsType(uploadFileType, file)) {
			return Result.error("文件类型不允许上传!file=" + file.getOriginalFilename());
		}

		String oriName = file.getOriginalFilename();
		String fileName = rename ? getFileName(file) : oriName;
		directory = getDirectory(directory, date);

		if (root.endsWith("/")) {
			root = root.substring(0, root.length() - 1);
		}

		// 文件上传后的路径
		String filePath = String.format("%s/%s/%s/", root, uploadPath, directory);
        File dest = new File(filePath + fileName);
        // 检测是否存在目录，不存在则创建
        if (!dest.getParentFile().exists()) {
            //dest.getParentFile().mkdirs();
			cn.hutool.core.io.FileUtil.mkdir(dest.getParentFile());
        }
        try {
			String extName = getExtName(file);
			// 如果设置了scale 且为图片，压缩图片
			float scale = getScale(file.getSize());
			if (scale < 1 && BaseEnum.has(SCALE_TYPES, extName)) {
				Img.from(ImgUtil.read(file.getInputStream())).setTargetImageType(extName).scale(scale).write(dest);
			} else {
				file.transferTo(dest);
			}
			String url = String.format("%s/%s/%s/%s", uploadUrl, uploadPath, directory, fileName);
			return Result.ok(new UploadBack(url, oriName, file.getSize(), dest));
        } catch (IllegalStateException | IOException e) {
			log.error("文件上传异常!e={}", e.getMessage());
			return Result.error(e.getMessage());
        }
	}

	/**
	 * 分片上传到阿里云OSS
	 * @param directory 文件夹
	 * @param file 文件
	 * @return 返回上传后的文件信息
	 */
	public Result<UploadBack> chuckUploadAliyunOss(String directory, MultipartFile file) {
		OSS ossClient = getInstance();
		if (null == ossClient) {
			return Result.error("ossClient为null错误");
		}
		String path = aliyunDnsUrl;
		if (StrUtil.isEmpty(path)) {
			path = aliyunEndpoint.replace("http://", "http://" + aliyunBucket);
		}
		String oriName = file.getOriginalFilename();
		String fileName = getFileName(file);
		directory = getDirectory(directory, true);
		String objectName = directory + "/" + fileName;
		try {
			// 创建InitiateMultipartUploadRequest对象。
			InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(aliyunBucket, objectName);

			// 返回uploadId，它是分片上传事件的唯一标识。您可以根据该uploadId发起相关的操作，例如取消分片上传、查询分片上传等。
			String uploadId = ossClient.initiateMultipartUpload(request).getUploadId();

			// partETags是PartETag的集合。PartETag由分片的ETag和分片号组成。
			List<com.aliyun.oss.model.PartETag> partETags =  new ArrayList<>();
			// 每个分片的大小，用于计算文件有多少个分片。单位为字节。
			final long partSize = 5 * 1024 * 1024L;   //5 MB。

			// 填写本地文件的完整路径。如果未指定本地路径，则默认从示例程序所属项目对应本地路径中上传文件。
			long fileLength = file.getSize();
			int partCount = (int) (fileLength / partSize);
			if (fileLength % partSize != 0) {
				partCount++;
			}
			// 遍历分片上传。
			for (int i = 0; i < partCount; i++) {
				long time = System.currentTimeMillis();
				long startPos = i * partSize;
				long curPartSize = (i + 1 == partCount) ? (fileLength - startPos) : partSize;
				InputStream inputStream = file.getInputStream();
				// 跳过已经上传的分片。
				inputStream.skip(startPos);
				com.aliyun.oss.model.UploadPartRequest uploadPartRequest = new com.aliyun.oss.model.UploadPartRequest();
				uploadPartRequest.setBucketName(aliyunBucket);
				uploadPartRequest.setKey(objectName);
				uploadPartRequest.setUploadId(uploadId);
				uploadPartRequest.setInputStream(inputStream);
				// 设置分片大小。除了最后一个分片没有大小限制，其他的分片最小为100 KB。
				uploadPartRequest.setPartSize(curPartSize);
				// 设置分片号。每一个上传的分片都有一个分片号，取值范围是1~10000，如果超出此范围，OSS将返回InvalidArgument错误码。
				uploadPartRequest.setPartNumber( i + 1);
				// 每个分片不需要按顺序上传，甚至可以在不同客户端上传，OSS会按照分片号排序组成完整的文件。
				com.aliyun.oss.model.UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
				// 每次上传分片之后，OSS的返回结果包含PartETag。PartETag将被保存在partETags中。
				partETags.add(uploadPartResult.getPartETag());
				log.debug("分片上传,i={},time={}", i, (System.currentTimeMillis() - time) + "ms");
			}
			long time = System.currentTimeMillis();
			// 创建CompleteMultipartUploadRequest对象。
			// 在执行完成分片上传操作时，需要提供所有有效的partETags。OSS收到提交的partETags后，会逐一验证每个分片的有效性。当所有的数据分片验证通过后，OSS将把这些分片组合成一个完整的文件。
			com.aliyun.oss.model.CompleteMultipartUploadRequest completeMultipartUploadRequest =
					new com.aliyun.oss.model.CompleteMultipartUploadRequest(aliyunBucket, objectName, uploadId, partETags);

			// 如果需要在完成分片上传的同时设置文件访问权限，请参考以下示例代码。
			completeMultipartUploadRequest.setObjectACL(CannedAccessControlList.PublicRead);

			// 完成分片上传。
			com.aliyun.oss.model.CompleteMultipartUploadResult result = ossClient.completeMultipartUpload(completeMultipartUploadRequest);

			log.debug("分片上传,result={},time={}", result, (System.currentTimeMillis() - time) + "ms");

			String url = String.format("%s/%s/%s", path, directory, fileName);
			return Result.ok(new UploadBack(url, oriName, file.getSize()));
		} catch (OSSException | ClientException | IORuntimeException | IOException e) {
			log.error("文件上传异常!e={}", e.getMessage());
			return Result.error(e.getMessage());
		}
	}

	/**
	 * <p>上传文件</p>
	 * @param directory 目录
	 * @param file 上传的文件
	 * @return 返回上传后的文件信息
	 */
	public Result<UploadBack> uploadAliyunOss(String directory, MultipartFile file) {
		return uploadAliyunOss(aliyunBucket, directory, file);
	}

	/**
	 * <p>上传文件</p>
	 * @param bucket 空间
	 * @param directory 目录
	 * @param file 上传的文件
	 * @return 返回上传后的文件信息
	 */
	public Result<UploadBack> uploadAliyunOss(String bucket, String directory, File file) {
		if (FileUtil.isEmpty(file)) {
			return Result.error("参数错误!file=null");
		}
		if (!containsType(uploadFileType, file)) {
			return Result.error("文件类型不允许上传!file=" + file.getName());
		}
		String fileName = getFileName(file);
		OSS ossClient = getInstance();
		if (null == ossClient) {
			return Result.error("ossClient为null错误");
		}
		String oriName = file.getName();
		String path = aliyunDnsUrl;
		if (StrUtil.isEmpty(path)) {
			path = aliyunEndpoint.replace("http://", "http://" + bucket);
		}
		try {
			PutObjectResult result = ossClient.putObject(new PutObjectRequest(bucket,
					directory + "/" + fileName, FileUtil.getInputStream(file)));
			ossClient.setBucketAcl(bucket, CannedAccessControlList.PublicRead);
			if (null == result) {
				return Result.error("ossClient.putObject返回null异常!");
			}
			String url = String.format("%s/%s/%s", path, directory, fileName);
			return Result.ok(new UploadBack(url, oriName, FileUtil.size(file)));
		} catch (OSSException | ClientException e) {
			log.error("文件上传异常!e={}", e.getMessage());
			return Result.error(e.getMessage());
		}
	}

	/**
	 * <p>上传文件</p>
	 * @param bucket 空间
	 * @param directory 目录
	 * @param file 上传的文件
	 * @return 返回上传后的文件信息
	 */
	public Result<UploadBack> uploadAliyunOss(String bucket, String directory, MultipartFile file) {
		if (file == null || file.isEmpty()) {
			return Result.error("参数错误!file=null");
		}
		if (!containsType(uploadFileType, file)) {
			return Result.error("文件类型不允许上传!file=" + file.getOriginalFilename());
		}
		String fileName = getFileName(file);
		OSS ossClient = getInstance();
		if (null == ossClient) {
			return Result.error("ossClient为null错误");
		}
		String oriName = file.getOriginalFilename();
		String path = aliyunDnsUrl;
		if (StrUtil.isEmpty(path)) {
			path = aliyunEndpoint.replace("http://", "http://" + bucket);
		}
		try {
			PutObjectResult result = ossClient.putObject(new PutObjectRequest(bucket,
					directory + "/" + fileName, file.getInputStream()));
			ossClient.setBucketAcl(bucket, CannedAccessControlList.PublicRead);
			if (null == result) {
				return Result.error("ossClient.putObject返回null异常!");
			}
			String url = String.format("%s/%s/%s", path, directory, fileName);
			return Result.ok(new UploadBack(url, oriName, file.getSize()));
		} catch (OSSException | ClientException | IOException e) {
			log.error("文件上传异常!e={}", e.getMessage());
			return Result.error(e.getMessage());
		}
	}

	/**
	 * 获得Oss私有地址
	 * @param url 地址
	 * @param second 秒
	 * @return 可以访问地址
	 */
	public String getOssPrivateUrl(String url, int second) {
		return getOssPrivateUrl(aliyunBucket, url, second);
	}

	/**
	 * 获得Oss私有地址
	 * @param bucket 空间
	 * @param url 地址
	 * @param second 秒
	 * @return 可以访问地址
	 */
	public String getOssPrivateUrl(String bucket, String url, int second) {
		String key = url;
		if (key.contains("?")) {
			key = key.substring(0, key.indexOf("?"));
		}
		if (key.startsWith("http")) {
			key = key.substring(8);
		}
		key = key.substring(key.indexOf("/") + 1);
		URL newUrl = getInstance().generatePresignedUrl(bucket, key, DateUtil.offsetSecond(new Date(), second));
		return newUrl.toString();
	}

	public static String getDirectory(String directory, boolean date) {
		if (StrUtil.isEmpty(directory)) {
			return DateUtil.format(new Date(), DatePattern.SIMPLE_MONTH_FORMAT);
		} else {
			if (date) {
				return directory + "/" + DateUtil.format(new Date(), DatePattern.SIMPLE_MONTH_FORMAT);
			} else {
				return directory;
			}
		}
	}

	// 解决中文问题，linux下中文路径，图片显示问题
	public static String getFileName(MultipartFile file) {
		return IdUtil.fastSimpleUUID() + "." + getExtName(file);
	}

	// 获取文件类型
	public static String getExtName(MultipartFile file) {
		if (file == null || file.getOriginalFilename() == null) return "";
		String fileName = file.getOriginalFilename().toLowerCase();
		return cn.hutool.core.io.FileUtil.extName(fileName);
	}

	// 验证是否存在容许文件类型
	public static boolean containsType(String uploadFileType, MultipartFile file) {
		List<String> types = StrUtil.split(uploadFileType, ",");
		String fileType = getExtName(file);
		return types.contains(fileType);
	}

	// 解决中文问题，linux下中文路径，图片显示问题
	public static String getFileName(File file) {
		return IdUtil.fastSimpleUUID() + "." + getExtName(file);
	}

	// 获取文件类型
	public static String getExtName(File file) {
		if (file == null) return "";
		return cn.hutool.core.io.FileUtil.extName(file);
	}

	// 验证是否存在容许文件类型
	public static boolean containsType(String uploadFileType, File file) {
		List<String> types = StrUtil.split(uploadFileType, ",");
		String fileType = getExtName(file);
		return types.contains(fileType);
	}
}